﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using BobcatUtility;

namespace BobcatMultiTouch
{
    /// <summary>
    /// This class listens for multitouch inputs on behalf of one form or control. It raises
    /// events when touch-events occur (down, up, move). The events include the touch point
    /// in both screen coordinates and client coordinates. The events are marshaled into the
    /// form's thread.
    /// </summary><remarks>
    /// Only touches that originally had a touch-down in the control will be raised to the control.
    /// The MTIM raises events in screen coordinates, and this class receives all of them. When it 
    /// receives a touch-down event it checks if the touch occured in the client area of the control
    /// we are managing. If it is outside the area of our client control we ignore it. If it is in
    /// our client area, we then "own" that touch, and we will raise all future events for it to
    /// out control (even if subsequent events are outside the client area).
    /// 
    /// Threading
    /// ---------
    /// The touch-events that this class receives may occur on a background thread. But some methods
    /// such as getActiveTouches, may occur on a different thread. Because of this, the data held by 
    /// this class is protected by a lock.
    /// </remarks>
    public class MultiTouchInputClient
    {
        #region Events

        /// <summary>
        /// Event raised when a touch down event occurs.
        /// </summary>
        public event EventHandler<TouchArgs> TouchDownEvent;

        /// <summary>
        /// Event raised when a touch up event occurs.
        /// </summary>
        public event EventHandler<TouchArgs> TouchUpEvent;

        /// <summary>
        /// Event raised when a mopve event occurs.
        /// </summary>
        public event EventHandler<TouchArgs> TouchMoveEvent;

        #endregion

        #region Public methods

        /// <summary>
        /// Constructor.
        /// </summary>
        public MultiTouchInputClient()
        {
        }

        /// <summary>
        /// Initializes this object and sets the control (or form) that it will
        /// manage touch events for. Events will be passed data in client coordinates
        /// for this control.
        /// </summary>
        public void initialize(Control control)
        {
            m_Control = control;
            m_iControlHWnd = control.Handle.ToInt32();

            // We register for events from the MTIM...
            MultiTouchInputManager mtim = MultiTouchInputManager.getInstance();
            mtim.TouchDownEvent += onTouchDown;
            mtim.TouchUpEvent += onTouchUp;
            mtim.TouchMoveEvent += onTouchMove;
        }

        /// <summary>
        /// Shuts down this object and unregisters for events.
        /// </summary>
        public void close()
        {
            // We unregister from events...
            MultiTouchInputManager mtim = MultiTouchInputManager.getInstance();
            mtim.TouchDownEvent -= onTouchDown;
            mtim.TouchUpEvent -= onTouchUp;
            mtim.TouchMoveEvent -= onTouchMove;
        }

        /// <summary>
        /// Returns the set of active touch points for the control we're managing.
        /// </summary>
        public IList<TouchArgs> getActiveTouches()
        {
            lock (m_Lock)
            {
                IList<TouchArgs> results = new List<TouchArgs>();
                foreach (KeyValuePair<int, TouchArgs> pair in m_setOwnedTouches)
                {
                    results.Add(pair.Value);
                }
                return results;
            }
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Called when we recieve a touch-down event. If it is in the client area of our
        /// control we start to "own" this touch, and we raise the event to our observers.
        /// </summary>
        private void onTouchDown(object sender, TouchArgs e)
        {
            lock (m_Lock)
            {
                // We only raise touch-down events if we're managing an active control...
                if (canReceiveInput() == false)
                {
                    return;
                }

                // We find the screen bounds of the control we're managing and see if the 
                // touch is inside them...
                Rectangle screenBounds = Utility.getScreenBounds(m_iControlHWnd);
                if (screenBounds.Contains(e.ScreenPosition) == false)
                {
                    // The touch is not in our client area...
                    return;
                }

                // We convert the screen coords into client coords, and note that we own the point...
                e.ClientPosition = toClientPoint(e.ScreenPosition, screenBounds);
                m_setOwnedTouches[e.PointIndex] = e.copy();

                // We fire the event to tell our observers...
                Utility.CrossThreadInvoke(TouchDownEvent, this, e);
            }
        }

        /// <summary>
        /// Called when a touch-up event is received. If we own it, we raise the event to
        /// our observers.
        /// </summary>
        private void onTouchUp(object sender, TouchArgs e)
        {
            lock(m_Lock)
            {
                // Do we own the point?
                if (m_setOwnedTouches.ContainsKey(e.PointIndex) == false)
                {
                    // We don't own the point...
                    return;
                }

                // We own the point, so we convert the point to client coords and raise
                // the event...
                Rectangle screenBounds = Utility.getScreenBounds(m_iControlHWnd);
                e.ClientPosition = toClientPoint(e.ScreenPosition, screenBounds);
                Utility.CrossThreadInvoke(TouchUpEvent, this, e);

                // We clear our ownership of this point...
                m_setOwnedTouches.Remove(e.PointIndex);
            }
        }

        /// <summary>
        /// Called when we receive a touch-move event. If we own the point we raise the event
        /// to our observers.
        /// </summary>
        private void onTouchMove(object sender, TouchArgs e)
        {
            lock (m_Lock)
            {
                // Do we own the point?
                int iPointIndex = e.PointIndex;
                if (m_setOwnedTouches.ContainsKey(iPointIndex) == false)
                {
                    // We don't own the point...
                    return;
                }

                // We own the point, so we convert the point to client coords update
                // the info we hold for the point...
                Rectangle screenBounds = Utility.getScreenBounds(m_iControlHWnd);
                e.ClientPosition = toClientPoint(e.ScreenPosition, screenBounds);
                TouchArgs touchArgs = m_setOwnedTouches[iPointIndex] = e.copy();

                // We raise the event to tell our observers...
                Utility.CrossThreadInvoke(TouchMoveEvent, this, e);
            }
        }

        /// <summary>
        /// Returns whether the control we are managing can receive input. This means that it
        /// is part of the active window.
        /// </summary>
        private bool canReceiveInput()
        {
            // We see if the top-level form that this control is a member of is active...
            return (Form.ActiveForm == m_Control.TopLevelControl);
        }

        /// <summary>
        /// We return the point relative to the client area for the point passed in.
        /// </summary>
        private Point toClientPoint(Point screenPoint, Rectangle screenBounds)
        {
            Point results = new Point();
            results.X = screenPoint.X - screenBounds.X;
            results.Y = screenPoint.Y - screenBounds.Y;
            return results;
        }
        
        #endregion

        #region Private data

        // The handle of the control (or form) we're managing. We need to keep this reference so that we can
        // translate coordinates into client coordinates for this control.
        private Control m_Control = null;
        private int m_iControlHWnd = 0;

        // The "set" of all touch-index that we own (i.e. that had a touch-down in our client area).
        private object m_Lock = new object();
        private IDictionary<int, TouchArgs> m_setOwnedTouches = new Dictionary<int, TouchArgs>();

        #endregion

    }
}
